通用技术 —— 依赖注入和控制反转

做Java Web开发的小伙伴对这两个概念应该再熟悉不过了,但是移动端的小伙伴可能就会陌生些。下面通过实例解释下这两个概念。

1. 什么是依赖

通过下面这个例子介绍下依赖的概念。

1
2
3
4
5
6
7
8
public class Human {
...
Father father;
...
public Human() {
father = new Father();
}
}

可以看出,类Human里有个Father的实例,我们就称Human对Father有依赖。

这种简单粗暴的依赖方式会有什么问题呢?
(1)耦合
Father的实例化方式在Human里固定了。如果要改变Father的实例化方式,如需要用new Father(String name)实例化Father,需要修改Human代码,违反了开闭原则。

(2)测试困难
由1衍生的问题,如果想测试不同Father对象对Human的影响很困难,因为 Father的初始化被写死在了 Human 的构造函数中;如果new Father()过程非常缓慢,单元测试时我们希望用已经初始化好的 Father对象mock掉这个过程也很困难

2. 依赖注入

2.1 什么是依赖注入(Dependency Injection,缩写为DI)

上面将依赖直接在构造中初始化使得两个类不够独立,且不方便测试。看看下面的初始化方式:

1
2
3
4
5
6
7
8
public class Human {
...
Father father;
...
public Human(Father father) {
this.father = father;
}
}

区别是,被依赖的Father对象是外部传入的。像这种不是内部直接初始化,而是外部传入的依赖方式,就称为依赖注入。

依赖注入的好处:
(1) 依赖之间一定程度的解耦
(2) 方便测试

当然,如果把Father抽象化,比如抽象出来个IFather接口,Human依赖IFather接口会更灵活些。

2.2 Java 中的依赖注入

依赖注入的实现有多种途径,而在 Java 中,使用注解是最常用的。比如通过在字段的声明前添加 @Inject 注解进行标记,来实现依赖对象的自动注入。

1
2
3
4
5
6
7
public class Human {
...
@Inject Father father;
...
public Human() {
}
}

上面这段代码看起来很神奇:只是增加了一个注解,Father 对象就能自动注入了?这个注入过程是怎么完成的?
实质上,如果你只是写了一个 @Inject 注解,Father 并不会被自动注入。你还需要使用一个依赖注入框架,并进行简单的配置。
现在 Java 语言中较流行的依赖注入框架有 Google GuiceSpring等,而在 Android 上比较流行的有 RoboGuiceDagger等。

2.3 简单的DI框架实现

下面通过一个简单的实例看看DI框架如何实现依赖注入的。

比如一个Activity里面有很多个View,如何实例化这些View呢?
常规做法:先给Activity使用setContentView()设置布局文件,然后在onCreate()里面逐个的findViewById()进行实例化。
依赖注入的做法:Activity类上添加个注解,帮我们自动注入布局;声明View的时候,添加一行注解,然后自动帮我们findViewById。

通过一个简单的例子看看如何实现DI框架,实现方式有2步。

第一步:自定义注解

1
2
3
4
5
@Target(ElementType.FIELD) 
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();
}
1
2
3
4
5
@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
int value();
}

第二步:解析注解,依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class DICore {

private static final String METHOD_SET_CONTENT_VIEW = "setContentView";
private static final String METHOD_FIND_VIEW_BY_ID = "findViewById";

//注入布局文件
private static void injectContentView(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 查询类上是否存在ContentView注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView == null) {
return;
}

int contentViewLayoutId = contentView.value();
try {
Method method = clazz.getMethod(METHOD_SET_CONTENTVIEW, int.class);
method.setAccessible(true);
method.invoke(activity, contentViewLayoutId);
} catch (Exception e) {
...
}
}

//注入View控件
private static void injectViews(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
Field[] fields = clazz.getDeclaredFields();

// 遍历所有成员变量
for (Field field : fields) {
ViewInject viewInjectAnnotation = field.getAnnotation(ViewInject.class);
if (viewInjectAnnotation == null) {
continue;
}

int viewId = viewInjectAnnotation.value();
if (viewId == -1) {
continue;
}

// 初始化View 
try {
Method method = clazz.getMethod(METHOD_FIND_VIEW_BY_ID, int.class);
Object resView = method.invoke(activity, viewId);
field.setAccessible(true);
field.set(activity, resView);
} catch (Exception e) {
...
}
}
}
}

如何使用?

1
2
3
4
5
6
7
8
9
10
11
12
@ContentView(value = R.layout.activity_main ) 
public class MainActivity extends Activity {
     
     @ViewInject(R.id.layout)
     private LinearLayout layout;
     
     @Override
     protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           ViewInjectUtils.inject(this); 
     }
}

3. 控制反转

控制反转(Inversion of Control,缩写为IoC)是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的实现方式是依赖注入。
通过控制反转,一个类获取依赖就不需要自己实现了,而是通过一个IoC容器,将这个类所依赖的对象的引用传递给这个它。也可以说,依赖被注入到对象中。

IoC和DI这两种概念的区别:
控制反转是一种设计思想,而依赖注入是实现这种思想的实现方式。

实现控制反转主要有两种方式:
依赖注入和依赖查找。
两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。

依赖注入有如下实现方式:
• 基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
• 基于 set 方法。实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
• 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
• 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。

依赖查找:
依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。

比如Android中常用的Context#getSystemService()方法:

1
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);

通过getSystemService指定需要依赖的服务名称即可获取依赖服务的实例,通过依赖查找也解决了服务创建和使用的分离。从而达到解耦的目的。

参考:
Inversion of Control Containers and the Dependency Injection pattern
依赖注入